Skip to content
View Article Network
Article Series:ASP.NET Core Web API 入門心得 (1 / 5)

Getting Started with ASP.NET Core Web API

Introduction

A client asked me to explain the Web API project currently under development. Initially, I only intended to explain that specific project, but the client also expressed interest in learning how to easily create and develop an ASP.NET Core Web API. While brainstorming for the presentation, I couldn't think of a suitable way to present it, so I decided to write a set of notes focusing on general, project-agnostic fundamental knowledge.

This article only covers the more basic or commonly used parts. For complete details, please refer to the MSDN Create Web APIs with ASP.NET Core documentation.

Creating a Project

I usually don't create a project directly; instead, I start by creating a "Blank Solution." If you cannot find the "Blank Solution" option in Visual Studio, you can check the Visual Studio Installer to see if the project templates are selected, such as "Other Project Templates (Legacy)" or ".NET Framework projects and project templates." I recall it being the former, but I am not entirely certain.

aspnet core webapi project template

When creating a Web API project, select "ASP.NET Core Web API."

select webapi template

By convention, I place the project path under a "src" folder.

project path setup

Here is an explanation of the settings under "Additional information" in the image below:

additional information settings

  1. Enable OpenAPI support: Checking this will install "Swashbuckle.AspNetCore" by default and add Swagger-related code in "Program.cs."

    enable openapi support

  2. Do not use top-level statements: Top-level statements are a feature added in C# 9.0. Simply put, programs usually use a Main method as an entry point, but when top-level statements are used, the code in the "Program.cs" file no longer requires a Program class and a Main method. This design aims to simplify the code structure. For more detailed information, please refer to Top-level statements - programs without Main methods.

  3. Use controllers: If not checked, it will use the Minimal API approach. For details, please refer to Minimal APIs overview.

  4. Create a solution folder to organize projects:

    solution folder structure

    Move the project under the "src" solution folder:

    move project to src

Code Explanation

When creating a Web API project, the following sample code is generated by default. I will explain this part next.

default generated code

ControllerBase

Unlike ASP.NET Framework, the MVC and Web API Controllers in ASP.NET Core do not originate from different assemblies but share the same base assembly. In ASP.NET Core, MVC inherits from Controller by default, while Web API inherits from the higher-level ControllerBase. Controller provides additional APIs related to Views, as well as three Filter-related events: OnActionExecuting, OnActionExecuted, and OnActionExecutionAsync.

If a Web API Controller needs to support both Views and Web API, or if you need to handle events like OnActionExecuting within the Controller, you can change the inheritance to Controller to meet those requirements.

ApiController

You can see that the ApiController attribute is set on the Web API Controller. According to MSDN, it performs the following behaviors:

  • Attribute routing requirement.
  • Automatic HTTP 400 responses.
  • Binding source parameter inference.
  • Multipart/form-data request inference.
  • Problem details for error status codes.

If you do not want to add the ApiController attribute to every Controller, you can achieve this by defining a BasicController base class. This BasicController can inherit from Controller or ControllerBase, and then have all individual Web API Controllers inherit from BasicController.

csharp
// Define the BasicController base class
[ApiController]
public class BasicController : ControllerBase {
    // You can add shared logic or properties here
}

// Specific Web API Controller inherits from BasicController
public class MyController : BasicController {
    // Implement Web API logic here
}

Additionally, if you wish to disable some behaviors of ApiController, you can adjust them in Program.cs as follows:

csharp
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options => {
        // Disable automatic HTTP 400 responses
        options.SuppressModelStateInvalidFilter = true;

        // Disable multipart/form-data request inference
        options.SuppressConsumesConstraintForFormFileParameters = true;

        // Disable binding source parameter inference
        options.SuppressInferBindingSourcesForParameters = true;

        // Disable problem details for error status codes
        options.SuppressMapClientErrors = true;
    });

Attribute Routing Requirement

In ASP.NET Core Web API, after setting ApiController, it switches to using the Route attribute for conventional routing configuration. The Route attribute can be set on the base Controller, the current Controller, or specific Actions, with priority from high to low: Action > Controller > Base Controller. Below are some common configuration examples:

RESTful Style

csharp
[Route("[controller]")]
public class MyController : BasicController {
    // GET: /My
    [HttpGet]
    public IActionResult Get() {
        // Implement logic to get resources
        return Ok("GET method");
    }

    // GET: /My/1
    [HttpGet("{id}")]
    public IActionResult GetById(int id) {
        // Implement logic to get a specific resource
        return Ok($"GET method with id {id}");
    }

    // POST: /My
    [HttpPost]
    public IActionResult Post([FromBody] MyModel model) {
        // Implement logic to add a resource
        return Ok("POST method");
    }

    // PUT: /My/1
    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody] MyModel model) {
        // Implement logic to update a resource
        return Ok($"PUT method with id {id}");
    }

    // PATCH: /My/1
    [HttpPatch("{id}")]
    public IActionResult Patch(int id, [FromBody] MyPatchModel model) {
        // Implement logic to partially update a resource
        return Ok($"PATCH method with id {id}");
    }

    // DELETE: /My/1
    [HttpDelete("{id}")]
    public IActionResult Delete(int id) {
        // Implement logic to delete a resource
        return Ok($"DELETE method with id {id}");
    }
}

Non-RESTful Style

csharp
[Route("[controller]/[action]")]
public class MyController : BasicController {
    // GET: /My/GetAll
    [HttpGet]
    public IActionResult GetAll() {
        // Implement logic to get resources
        return Ok("GET method");
    }

    // GET: /My/GetById/1
    [HttpGet("{id}")]
    public IActionResult GetById(int id) {
        // Implement logic to get a specific resource
        return Ok($"GET method with id {id}");
    }

    // POST: /My/Create
    [HttpPost]
    public IActionResult Create([FromBody] MyModel model) {
        // Implement logic to add a resource
        return Ok("POST method");
    }

    // POST: /My/Update/1
    [HttpPost("{id}")]
    public IActionResult Update(int id, [FromBody] MyModel model) {
        // Implement logic to update a resource
        return Ok($"PUT method with id {id}");
    }

    // POST: /My/Delete/1
    [HttpPost("{id}")]
    public IActionResult Delete(int id) {
        // Implement logic to delete a resource
        return Ok($"DELETE method with id {id}");
    }
}

Adding a Prefix

csharp
[Route("api/[controller]")]
public class MyController : BasicController {
    // GET: api/My
    [HttpGet]
    public IActionResult Get() {
        // Implement logic to get resources
        return Ok("GET method");
    }
}

It is worth noting that in the past, in ASP.NET Framework Web API, HTTP verbs could be identified by the beginning of the Action name (e.g., GetAll() corresponded to a GET request), but now it has changed to use HttpGet, HttpPost, HttpPut, HttpPatch, and HttpDelete attributes to explicitly identify them, just like in MVC. When no attribute is set, it defaults to a GET request.

TIP

  • When the ApiController attribute is set, you cannot access actions through conventional routing defined by methods like UseEndpoints(), UseMvc(), or UseMvcWithDefaultRoute().
  • When using methods like UseMvc() to set up routing, conventional routing uses curly braces {} to represent parameters, e.g., "{controller=Home}/{action=Index}/{id?}"; whereas in the Route attribute, square brackets [] are used, e.g., "[controller]/[action]".

Automatic HTTP 400 Responses

In ASP.NET Core Web API, the automatic HTTP 400 response mechanism eliminates the need to manually validate the Request Model's validity. This feature is implemented through the built-in ModelStateInvalidFilter, so you no longer need to manually execute the following code for validation as you did in the past ASP.NET Web API era:

csharp
if (!ModelState.IsValid) {
    return BadRequest(ModelState);
}

Binding Source Parameter Inference

ASP.NET Core provides the following attributes for setting how parameters are bound:

  • FromBody: Request body. This attribute is used to bind data from the HTTP request body, typically used for POST requests where data is sent via the request body.
  • FromForm: Form data in the request body. Using this attribute allows binding data from HTML forms, typically used for POST requests where data is submitted in form format.
  • FromHeader: Request header. This attribute is used to extract data from HTTP request headers, such as extracting the value of a specific header.
  • FromQuery: Request query string parameters. Using this attribute allows binding data from the URL query string, typically used for GET requests.
  • FromRoute: Route data from the current request. This attribute is used to bind data extracted from the route, typically used for route parameters defined in the route.
  • FromServices: Request service injected as an action parameter. Using this attribute allows binding services from the DI (Dependency Injection) container, making them available for use in action methods.

The automatic inference rules are as follows:

  • FromBody: Automatically infers complex type parameters not registered in the DI container, but ignores some special built-in types like IFormCollection and CancellationToken.
  • FromForm: Specifically inferred for parameters of types IFormFile and IFormFileCollection. It does not infer any simple or custom types.
  • FromRoute: Inferred based on parameter names that match route template parameters. If multiple routes match the parameter, the system treats it as FromRoute.
  • FromQuery: Inferred for any other parameters, rather than parameters specific to the route.

Multipart/Form-Data Request Inference

When the parameter type is IFormFile or IFormFileCollection, the request content is automatically inferred as multipart/form-data.

Problem Details for Error Status Codes

When the status code is 400 or higher, it returns the ProblemDetails type.

Return Types

In Web API, there are three commonly declared return types: concrete types, IActionResult, and ActionResult<T> (added in ASP.NET Core 2.1). If the API needs to return data, it is recommended to use ActionResult<T>. Conversely, if you only need to return status codes or similar information, use IActionResult. Here is how to use them:

Concrete Type

csharp
[HttpGet("{id}")]
public string GetById(int? id) {
    // This approach can only return data of a concrete type, not ActionResult
    // if (!id.HasValue)
    // {
    //     return BadRequest("Invalid id");
    // }

    // Return value directly
    return $"GET method with id {id}";
}

IActionResult

csharp
[HttpGet("{id}")]
public IActionResult GetById(int? id) {
    // Using IActionResult, you can return various ActionResult types
    if (!id.HasValue) {
        // Return 400 Bad Request
        return BadRequest("Invalid id");
    }

    // To return a value, wrap it with Ok()
    return Ok($"GET method with id {id}");
}
csharp
[HttpGet("{id}")]
public ActionResult<string> GetById(int? id) {
    // Using ActionResult<T>, you can also return various ActionResult types
    if (!id.HasValue) {
        // Return 400 Bad Request
        return BadRequest("Invalid id");
    }

    // Can also return the value directly
    return $"GET method with id {id}";
}

Asynchronous Implementation

In this code snippet, the async modifier indicates that this is an asynchronous method, and the return type is changed to Task<T>, which is a common convention. Usually, when adopting asynchronous methods, we are accustomed to adding "Async" to the end of the Action name to clearly indicate that the method is asynchronous.

It is worth noting that when using the non-RESTful style, regardless of whether the Action name ends with "Async", the Action in the route will not include "Async". For example: /My/GetById/1. The RESTful style is unaffected because it uses HTTP verbs for mapping, which is independent of the Action name.

csharp
[HttpGet("{id}")]
public async Task<IActionResult> GetByIdAsync(int id) {
    return await Ok(service.GetByIdAsync(id));
}

Swagger

Swagger is a powerful backend API visualization tool. Through Swagger UI, you can easily generate an interactive web-based API documentation while providing convenient API testing tools.

In ASP.NET Core Web API, the default Swagger package used is "Swashbuckle.AspNetCore". To learn more about the complete usage, please refer to Get started with Swashbuckle and ASP.NET Core. Here, I only summarize some parts I use frequently.

As mentioned earlier when creating a Web API project, when "Enable OpenAPI support" is checked, the code will automatically add the following:

csharp
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}
  • AddEndpointsApiExplorer(): This is a built-in ASP.NET Core method, only needed when Swagger needs to add support for Minimal APIs.

  • AddSwaggerGen(): Used to inject Swagger-related services into the DI container. Usually, you configure the Swagger generator in this method to extract API-related information from the application's assemblies, Controllers, and annotations.

  • UseSwagger(): Enables the Swagger Middleware, allowing it to provide Swagger documentation when the application is running. You can view the generated JSON file via "https://{Your Domain}/swagger/v1/swagger.json".

  • UseSwaggerUI(): Enables the Swagger UI Middleware, generating an interactive web interface. The default URL is "https://{Your Domain}/swagger/index.html". An example page is shown below:

    swagger ui example

    Swagger UI must rely on "swagger.json"; if UseSwagger() is not used, it will not work properly. You can use the following code to change the "swagger" part of the URL:

    csharp
    app.UseSwaggerUI(opt => {
        // Setting RoutePrefix only changes the UI URL, not the JSON location
        // Do not write "./swagger/v1/swagger.json", otherwise the path will become "https://{Your Domain}/test/swagger/v1/swagger.json" and be incorrect
        opt.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
        // The URL will become https://{Your Domain}/test/index.html
        opt.RoutePrefix = "test";
    });

    Additionally, you can use opt.DocumentTitle to set the title in the <head> tag of the webpage, or use opt.InjectStylesheet({Your CSS URL}) to load additional CSS styles (please ensure app.UseStaticFiles() is used in conjunction; details are omitted here).

WARNING

In ASP.NET Core Web API, even if HTTP attributes like HttpGet are not explicitly set, the API will be treated as a GET request. However, in Swagger, if an Action lacks the corresponding HTTP attribute, it may lead to the inability to correctly display relevant descriptions, thereby affecting the operation of Swagger UI. Therefore, when designing an API, do not design public methods that are not Actions, and explicitly add the corresponding HTTP attribute to each Action to ensure that Swagger can function properly.

Adding Extra Input Fields

If you wish to introduce extra parameters in the API design that are not passed through Action parameters, you can add corresponding input fields in Swagger in the following way:

Create a HeaderTokenFilter class, with the code as follows:

csharp
public class HeaderTokenFilter : IOperationFilter {
    public void Apply(OpenApiOperation operation, OperationFilterContext context) {
        operation.Parameters ??= new List<OpenApiParameter>();

        operation.Parameters.Add(new OpenApiParameter {
            Name = "Token",
            In = ParameterLocation.Header,
            Required = true,
            Schema = new OpenApiSchema {
                Type = "string"
            }
        });
    }

}

Integrate the following configuration in SwaggerGen() in Program.cs:

csharp
builder.Services.AddSwaggerGen(opt => {
    opt.OperationFilter<HeaderTokenFilter>();
});

The added Token input field will be displayed in Swagger UI:

swagger authorize input

API Information and Description

Setting API Author, Authorization, and Description

If you want to specify the API's author, authorization information, and description, you can set it through the following code:

csharp
builder.Services.AddSwaggerGen(opt => {
    opt.SwaggerDoc("v1", new OpenApiInfo {
        Version = "v1",
        Title = "Test API",
        Description = "This is a test sample.",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact {
            Name = "Example Contact",
            Url = new Uri("https://example.com/contact")
        },
        License = new OpenApiLicense {
            Name = "Example License",
            Url = new Uri("https://example.com/license")
        },
    });
});

The information presented in Swagger is as follows:

swagger api info display

The corresponding swagger.json content is as follows:

json
"info": {
    "title": "Test API",
    "description": "This is a test sample.",
    "termsOfService": "https://example.com/terms",
    "contact": {
      "name": "Example Contact",
      "url": "https://example.com/contact"
    },
    "license": {
      "name": "Example License",
      "url": "https://example.com/license"
    },
    "version": "v1"
}

Integrating Web API XML Comments into Swagger

Configure the following in csproj:

xml
<PropertyGroup>
  <!--Generate documentation file-->
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <!--Only set if you want to customize the path and filename-->
  <!--<DocumentationFile>D:\\Doc.xml</DocumentationFile>-->
  <!--If documentation is enabled, it will cause warnings for all public and protected members in the project without XML comments, so set to hide-->
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Integrate the following configuration in SwaggerGen() in Program.cs:

csharp
builder.Services.AddSwaggerGen(opt => {
    // If DocumentationFile is set, please change the XML location accordingly
    string xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    opt.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});

Add XML comments to the Action, for example:

csharp
/// <summary>
/// Gets information for a specific item.
/// </summary>
/// <param name="id">The unique identifier for the item.</param>
/// <returns>Information about the item.</returns>
/// <remarks>
/// Sample request:
///     GET: /My/1
/// </remarks>
/// <response code="400">If the identifier is null.</response>
[HttpGet("{id}")]
public ActionResult<string> GetById(int? id) {
    if (!id.HasValue) {
        return BadRequest();
    }
    return $"GET method with id {id}";
}

Swagger UI will display the corresponding comments:

swagger xml comments display

swagger.json will also add summary, description, and 400 content:

json
"get": {
    "tags": [
      "My"
    ],
    "summary": "Gets information for a specific item.",
    "description": "Sample request:\r\n    GET: /My/1",
    "parameters": [
      {
        "name": "id",
        "in": "path",
        "description": "The unique identifier for the item.",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int32"
        }
      }
    ],
    "responses": {
      "200": {
        "description": "Success",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            }
          },
          "application/json": {
            "schema": {
              "type": "string"
            }
          },
          "text/json": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "400": {
        "description": "If the identifier is null."
      }
    }
},

XML Comments on Input Model or Output Model

Add XML comments to the Input Model class, for example:

csharp
/// <summary>
/// Represents a test model.
/// </summary>
public class MyModel {
    /// <summary>
    /// Gets or sets the unique identifier.
    /// </summary>
    public int Id { get; set; }
}

Swagger UI will display the corresponding comments:

swagger response types

swagger.json will also add description content:

json
"components": {
    "schemas": {
      "MyModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Gets or sets the unique identifier.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Represents a test model."
      },
      "MyPatchModel": {
        "type": "object",
        "additionalProperties": false
      }
    }
}

Change Log

  • 2023-08-04 Initial version created.